/*
 * CCVisu is a tool for visual graph clustering
 * and general force-directed graph layout.
 * This file is part of CCVisu.
 *
 * Copyright (C) 2005-2011  Dirk Beyer
 *
 * CCVisu is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * CCVisu is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with CCVisu; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 * Please find the GNU Lesser General Public License in file
 * license_lgpl.txt or http://www.gnu.org/licenses/lgpl.txt
 *
 * Dirk Beyer    (firstname.lastname@uni-passau.de)
 * University of Passau, Bavaria, Germany
 */
package org.sosy_lab.ccvisu.ui;

import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.Window;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.InputEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.util.EventObject;
import java.util.HashSet;
import java.util.Set;

import javax.swing.JPanel;
import javax.swing.event.MouseInputAdapter;

import org.sosy_lab.ccvisu.CCVisuController;
import org.sosy_lab.ccvisu.CCVisuOptions;
import org.sosy_lab.ccvisu.graph.GraphEdge;
import org.sosy_lab.ccvisu.graph.GraphVertex;
import org.sosy_lab.ccvisu.graph.GraphicLayoutInfo;
import org.sosy_lab.ccvisu.graph.Position;
import org.sosy_lab.ccvisu.graph.interfaces.GraphEventListener;
import org.sosy_lab.ccvisu.ui.interfaces.FrameDisplayMouseAdapter;
import org.sosy_lab.ccvisu.writers.WriterDataLayoutDISP;
import org.sosy_lab.common.configuration.Configuration;
import org.sosy_lab.common.configuration.InvalidConfigurationException;
import org.sosy_lab.common.configuration.Option;
import org.sosy_lab.common.configuration.Options;

/**
 * Frame implementation for displaying the layout on the screen device.
 * Used by WriterDataGraphicsDISP.
 */
@Options(prefix="gui")
public class FrameDisplay implements GraphEventListener {

  private final Container          mFrame;
  private CCVisuOptions            options;

  private CCVisuController         controller;

  /** Canvas for graphics.*/
  private final GraphCanvas canvas;

  // Coordinates for zooming rectangle.
  private Point                    rectTopLeft              = new Point(0, 0);
  private Point                    rectBottomRight          = new Point(0, 0);
  private boolean                  rectShow                 = false;

  // Coordinates of the mouse when MOUSE_PRESSED.
  private int                      mouseX;
  private int                      mouseY;
  private int                      tolerance;

  // Node id when MOUSE_PRESSED.
  private GraphVertex              draggingVertex           = null;
  private GraphVertex              chosenVertex             = null;

  @Option(name="disableVertexDragging",
      description="disable vertex dragging")
  private boolean                  disableVertexDragging    = false;

  @Option(name="showRefreshingLabel",
      description="always inform the user that ccvisu is currently refreshing the screen")
  private boolean                  showRefreshingLabel      = true;

  private FrameDisplayMouseAdapter frameDisplayMouseAdapter = null;

  /**
   * Constructor.
   * @param writer      The writer that uses this frame to display the layout.
   * @throws InvalidConfigurationException
   */
  public FrameDisplay(final WriterDataLayoutDISP writer, Container frame,
      CCVisuController controller, Configuration config)
          throws InvalidConfigurationException {

    config.inject(this);
    options = writer.getOptions();
    this.controller = controller;

    if (frameDisplayMouseAdapter == null) {
      // Set default mouse listener that allows CCVisu to process all events by itself
      frameDisplayMouseAdapter= new AbstractFrameDisplayMouseAdapter();
    }

    // The frame.
    mFrame = frame;

    // The graphics canvas.
    canvas = new GraphCanvas(writer, this);

    // Add canvas to the frames content pane.
    mFrame.add(canvas);
    mFrame.addComponentListener(new ComponentListener() {
      @Override
      public void componentResized(ComponentEvent pE) {
        // Repaint the canvas after the frame has been resized.
        canvas.updateAndPaint();
      }

      @Override
      public void componentMoved(ComponentEvent e) { }

      @Override
      public void componentShown(ComponentEvent e) { }

      @Override
      public void componentHidden(ComponentEvent e) { }
    });

    // Show canvas.
    if (mFrame instanceof Window) {
      ((Window)mFrame).setVisible(true);
    }

  } // constructor

  /**
   * repaint when the graph change
   * @param evt a GraphEvent
   */
  @Override
  public void onGraphChangedEvent(EventObject evt) {
    this.canvas.updateAndPaint();
  }

  /**
   * @return the canvas
   */
  public GraphCanvas getCanvas() {
    return this.canvas;
  }

  public enum LoadDirection {
    NEXT, PREV
  }

  /**
   * Canvas implementation for displaying the layout on the screen.
   */
  public class GraphCanvas extends JPanel  {
    private static final long          serialVersionUID = 200510192212L;
    private final Component            parentFrame;
    private final WriterDataLayoutDISP writer;
    private FrameDisplayMenu           frameDisplayMenu;

    // image used for off-screen work
    private BufferedImage              img;
    // dimension of the image used
    private Dimension                  size             = new Dimension(0, 0);

    private boolean                    requiresReRender = true;
    /**
     * Constructor.
     * @param parentFrame The parent frame.
     * @param writer The writer that uses this object to draw on.
     *               The painting is delegated to the writer object.
     */
    private GraphCanvas(final WriterDataLayoutDISP writer,
        FrameDisplay parentFrameDisplay) {

      this.parentFrame = mFrame;
      this.writer = writer;

      // Creating the menu
      frameDisplayMenu = new FrameDisplayMenu(this, options.getGraph());
      add(frameDisplayMenu.createMenu());

      // Adds MouseMotionListener for mouse event ''Mouse moved on vertex''.
      // Show the name(s) of the vertex(vertices) in the vertexNameDialog.
      addMouseMotionListener(new MouseInputAdapter() {
        @Override
        public void mouseMoved(MouseEvent evt) {
          if (!frameDisplayMouseAdapter.mouseMoved(evt, writer)) {
            return;
          }

          if (options.isEnableToolTips()) {
            Set<String> verticesSelected = writer.getNames(evt.getPoint(), false);
            String vertexToolTip = null;

            if (verticesSelected.size() > 0) {
              vertexToolTip = writer.getGraphData().getVertexByName(verticesSelected.iterator().next()).getTooltip();
              if (vertexToolTip.length() > 100) {
                vertexToolTip = vertexToolTip.substring(0, 100) + "...";
              }
            }

            writer.getDisplay().getCanvas().setToolTipText(vertexToolTip);
          }
        }

        @Override
        public void mouseDragged(MouseEvent evt) {
          if (rectShow) {
            // Zooming rectangle, set end corner.
            rectBottomRight.setLocation(evt.getPoint());
            repaint();
          }

          // Vertex dragging
          if ((draggingVertex != null) && !disableVertexDragging) {
            Dimension lSize = getDrawingAreaSize();
            GraphicLayoutInfo graphicLayoutInfo = writer.calculateOffsetAndScale(lSize);

            Position eventPos = new Position(evt.getPoint().x, evt.getPoint().y, 0);
            Position vertexPos = graphicLayoutInfo.mapToLayout(eventPos);

            if (draggingVertex != null) {
              draggingVertex.setPosition(vertexPos);

              // Default implementation should always return true
              if (frameDisplayMouseAdapter.vertexDragged(evt, writer, draggingVertex)) {
                updateAndPaint();
              }
            }
          }
        }
      });

      // Adds MouseListener for mouse event ''Mouse clicked on vertex''.
      // Draw the name(s) of the vertex(vertices) as annotation on the canvas.
      addMouseListener(new MouseAdapter() {

        private void handlePopupTrigger(MouseEvent evt) {
          // MacOs X and Windows trigger different events when
          // the right mouse button is pressed.
          // Therefore the popup-menu code has been moved to a separate function.
          // See: http://developer.apple.com/library/mac/#documentation/Java/Conceptual/
          //    Java14Development/07-NativePlatformIntegration/NativePlatformIntegration.htm

          // Pop-up triggered -> show the PopupMenu
          Set<String> vertexRelations = new HashSet<String>();

          Set<String> tmp = writer.getNames(evt.getPoint(), false);
          if (!tmp.isEmpty()) {
            // Vertex chosen with pop-up menu
            chosenVertex = writer.getGraphData().getVertexByName(tmp.iterator().next());

            // Add relations to pop-up menu
            for (GraphEdge e : writer.getGraph().getAdjacent(chosenVertex, ".*")) {
              vertexRelations.add(e.getRelationName());
            }
          } else {
            chosenVertex = null;
          }

          frameDisplayMenu.updateAndShowMenu(evt, vertexRelations, chosenVertex);
        }

        @Override
        public void mousePressed(MouseEvent evt) {
          if (!frameDisplayMouseAdapter.mousePressed(evt, writer)) {
            return;
          }

          if (evt.isPopupTrigger()) {
            handlePopupTrigger(evt);
          } else if (evt.getButton() == MouseEvent.BUTTON1) {
            mouseX = evt.getX();
            mouseY = evt.getY();
            tolerance = Math.max(getHeight(), getWidth());
            tolerance /= 300;
            Set<String> tmp = writer.getNames(evt.getPoint(), false);
            if (tmp.isEmpty()) {
              rectTopLeft.setLocation(evt.getPoint());
              rectShow = true;

              draggingVertex = null;
            } else {
              // Vertex dragging.
              draggingVertex = writer.getGraph().getVertexByName(tmp.iterator().next());
            }
          }
        }

        @Override
        public void mouseReleased(MouseEvent evt) {
          if (evt.isPopupTrigger()) {
            handlePopupTrigger(evt);
          } else if (evt.getButton() == MouseEvent.BUTTON1) {
            int x = evt.getX();
            int y = evt.getY();
            if (Math.abs(x - mouseX) > tolerance || Math.abs(y - mouseY) > tolerance) {
              if (rectShow) {
                rectShow = false;
                rectBottomRight.setLocation(evt.getPoint());

                // Switch coordinates if top-left is not top-left.
                int xTl = Math.min(rectTopLeft.x, rectBottomRight.x);
                int xBr = Math.max(rectTopLeft.x, rectBottomRight.x);
                int yTl = Math.min(rectTopLeft.y, rectBottomRight.y);
                int yBr = Math.max(rectTopLeft.y, rectBottomRight.y);
                rectTopLeft.setLocation(xTl, yTl);
                rectBottomRight.setLocation(xBr, yBr);

                writer.restrictShowedVertices(rectTopLeft, rectBottomRight);
                updateAndPaint();
              }
            } else {
              rectShow = false;
              if (frameDisplayMouseAdapter != null) {
                if (!frameDisplayMouseAdapter.mouseReleased(evt, writer)) { return; }
              }

              if (writer.getOpenUrl()
                  && (evt.getModifiersEx() == InputEvent.CTRL_DOWN_MASK)) {
                writer.openURL(evt.getPoint());
              } else if (writer.toggleNames(evt.getPoint()) > 0) {
                //if something changed then recompute the img
                updateAndPaint();
              }
            }
          }
        }
      });
    }

    public Dimension getDrawingAreaSize() {
      int xSize = getSize().width - mFrame.getInsets().left - mFrame.getInsets().right;
      int ySize = getSize().height - mFrame.getInsets().top - mFrame.getInsets().bottom;

      Dimension size = null;
      if (writer.getStretchGraphToBounds()) {
        size = new Dimension(xSize, ySize);
      } else {
        int minSize = Math.min(xSize, ySize);
        size = new Dimension(minSize, minSize);
      }

      return size;
    }

    public void zoomOut() {
      writer.resetRestriction();
      canvas.updateAndPaint();
    }

    private Color previousBackgroundColor = null;

    /**
     * Draws the layout on the screen.
     * @param area  The graphics area for drawing.
     */
    @Override
    public void paint(Graphics area) {
      // Size info.
      setSize(parentFrame.getSize());

      // TODO: Enhance Performance. Some Tasks need only be done if
      // the graph data/layout has really changed.

      if (requiresReRender) {
        requiresReRender = false;

        // Adjust background color.
        if (options.getBackColor().get() != previousBackgroundColor) {
          setBackground(options.getBackColor().get());
          previousBackgroundColor = options.getBackColor().get();
        }

        // Draw "Refreshing..." in the center of the canvas.
        if (showRefreshingLabel) {
          // Calculate the center position.
          int x = this.getWidth() / 2 - 70;
          int y = this.getHeight() / 2;
          // Draw it.
          area.setColor(canvas.writer.getWriteColor());
          area.setFont(new Font("SansSerif", Font.ITALIC, 30));
          area.drawString("Refreshing...", x, y);
        }

        setSize(parentFrame.getSize());
        int xSize = getSize().width - mFrame.getInsets().left - mFrame.getInsets().right;
        int ySize = getSize().height - mFrame.getInsets().top - mFrame.getInsets().bottom;

        // (Re-)create the image buffer object.
        if (img == null || xSize != size.width || ySize != size.height) {
          size = new Dimension(xSize, ySize);
          img = new BufferedImage(size.width, size.height, BufferedImage.TYPE_INT_RGB);
        }

        // Enable anti-aliasing.
        // WARNING GCJ: Graphics2D needs  gcc 4
        Graphics2D imgArea = (Graphics2D) img.getGraphics();
        imgArea.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
        imgArea.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

        // Set the font-style.
        imgArea.setFont(new Font("SansSerif", Font.PLAIN, controller.getFontSize()));

        // Fill the background.
        imgArea.setColor(this.getBackground());
        imgArea.fillRect(0, 0, size.width, size.height);
        imgArea.setColor(Color.BLACK);

        // Get the size of the area to draw.
        Dimension drawingAreaSize = getDrawingAreaSize();

        // Draw the graph.
        writer.writeDISP(drawingAreaSize, imgArea, size.width, size.height, mFrame.getInsets().left,
            mFrame.getInsets().bottom);

        //free some resources
        imgArea.dispose();
      }

      //draw img on area
      area.drawImage(img, 0, 0, null);

      // Zooming rectangle.
      if (rectShow) {
        int x = (int) rectTopLeft.getX();
        int y = (int) rectTopLeft.getY();
        int width = (int) (rectBottomRight.getX() - rectTopLeft.getX());
        int height = (int) (rectBottomRight.getY() - rectTopLeft.getY());
        if (width < 0) {
          width = Math.abs(width);
          x = (int) rectBottomRight.getX();
        }
        if (height < 0) {
          height = Math.abs(height);
          y = (int) rectBottomRight.getY();
        }
        area.drawRect(x, y, width, height);
      }
    } // method paint

    /**
     * to use when changes are done and you want to display them
     *   e.g. the user wants to zoom into the graph.
     */
    public void updateAndPaint() {
      // Use of the graphics-object is only allowed inside the paint-method!
      // Graphics area = this.getGraphics();
      // // this.updateImage(area);
      this.requiresReRender = true;
      this.invalidate();
      this.repaint();
    }

    /**
     * @return the writer
     */
    public WriterDataLayoutDISP getWriter() {
      return this.writer;
    }

    /**
     * @return the parentFrame
     */
    public Component getParentFrame() {
      return this.parentFrame;
    }
  }
}
